{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "9be5ef4b",
   "metadata": {},
   "source": [
    "# 멀티 에이전트 감독자(Multi-Agent Supervisor)\n",
    "\n",
    "이 튜토리얼에서는 **LangGraph**를 활용하여 다중 에이전트 시스템을 구축하고, 에이전트 간 작업을 효율적으로 조정하고 감독자(Supervisor)를 통해 관리하는 방법을 살펴봅니다.  \n",
    "여러 에이전트를 동시에 다루며, 각 에이전트가 자신의 역할을 수행하도록 관리하고, 작업 완료 시 이를 적절히 처리하는 과정을 다룹니다.\n",
    "\n",
    "---\n",
    "\n",
    "**개요**\n",
    "\n",
    "이전 튜토리얼에서는 초기 연구자(Researcher) 에이전트의 출력에 따라 메시지를 자동으로 라우팅하는 방식을 보여주었습니다.  \n",
    "그러나 에이전트가 여러 개로 늘어나고, 이들을 조정해야 할 경우, 단순한 분기 로직만으로는 한계가 있습니다.  \n",
    "여기서는 [LLM을 활용한 Supervisor](https://langchain-ai.github.io/langgraph/concepts/multi_agent/#supervisor)를 통해 에이전트들을 관리하고, 각 에이전트 노드의 결과를 바탕으로 팀 전체를 조율하는 방법을 소개합니다.\n",
    "\n",
    "**중점 사항**:  \n",
    "- Supervisor는 다양한 전문 에이전트를 한 데 모아, 하나의 팀(team)으로 운영하는 역할을 합니다.  \n",
    "- Supervisor 에이전트는 팀의 진행 상황을 관찰하고, 각 단계별로 적절한 에이전트를 호출하거나 작업을 종료하는 등의 로직을 수행합니다.\n",
    "\n",
    "![](./assets/langgraph-multi-agent-supervisor.png)\n",
    "\n",
    "---\n",
    "\n",
    "**이 튜토리얼에서 다룰 내용**\n",
    "\n",
    "- **설정(Setup)**: 필요한 패키지 설치 및 API 키 설정 방법  \n",
    "- **도구 생성(Tool Creation)**: 웹 검색 및 플롯(plot) 생성 등, 에이전트가 사용할 도구 정의  \n",
    "- **도우미 유틸리티(Helper Utilities)**: 에이전트 노드 생성에 필요한 유틸리티 함수 정의  \n",
    "- **에이전트 감독자 생성(Creating the Supervisor)**: 작업자(Worker) 노드의 선택 및 작업 완료 시 처리 로직을 담은 Supervisor 생성  \n",
    "- **그래프 구성(Constructing the Graph)**: 상태(State) 및 작업자(Worker) 노드를 정의하여 전체 그래프 구성  \n",
    "- **팀 호출(Invoking the Team)**: 그래프를 호출하여 실제로 다중 에이전트 시스템이 어떻게 작동하는지 확인\n",
    "\n",
    "이 과정에서 LangGraph의 사전 구축된 [create_react_agent](https://langchain-ai.github.io/langgraph/reference/prebuilt/#langgraph.prebuilt.chat_agent_executor.create_react_agent) 함수를 활용해, 각 에이전트 노드를 간소화합니다. \n",
    "\n",
    "이러한 \"고급 에이전트\" 사용 방식은 LangGraph에서의 특정 디자인 패턴을 시연하기 위한 것이며, 필요에 따라 다른 기본 패턴과 결합하여 최적의 결과를 얻을 수 있습니다.\n",
    "\n",
    "---\n",
    "\n",
    "**참고**\n",
    "\n",
    "- [LangGraph 공식 문서](https://langchain-ai.github.io/langgraph/)  \n",
    "- [멀티 에이전트 Supervisor 개념](https://langchain-ai.github.io/langgraph/concepts/multi_agent/#supervisor)  \n",
    "- [create_react_agent 함수 문서화](https://langchain-ai.github.io/langgraph/reference/prebuilt/#langgraph.prebuilt.chat_agent_executor.create_react_agent)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3e51e124",
   "metadata": {},
   "source": [
    "## 환경 설정"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b86d4cf1",
   "metadata": {},
   "outputs": [],
   "source": [
    "# API 키를 환경변수로 관리하기 위한 설정 파일\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "# API 키 정보 로드\n",
    "load_dotenv()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "183d205f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n",
    "# !pip install -qU langchain-teddynote\n",
    "from langchain_teddynote import logging\n",
    "\n",
    "# 프로젝트 이름을 입력합니다.\n",
    "logging.langsmith(\"CH17-LangGraph-Use-Cases\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "eee7320a",
   "metadata": {},
   "source": [
    "본 튜토리얼에 사용할 모델명을 설정합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c0966fa3",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_teddynote.models import get_model_name, LLMs\n",
    "\n",
    "# 최신 버전의 모델명을 가져옵니다.\n",
    "MODEL_NAME = get_model_name(LLMs.GPT4)\n",
    "print(MODEL_NAME)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a4dde1a1",
   "metadata": {},
   "source": [
    "## 상태 정의\n",
    "\n",
    "멀티 에이전트 시스템에서 활용할 상태(state)를 정의합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "205bd008",
   "metadata": {},
   "outputs": [],
   "source": [
    "import operator\n",
    "from typing import Sequence, Annotated\n",
    "from typing_extensions import TypedDict\n",
    "\n",
    "from langchain_core.messages import BaseMessage\n",
    "\n",
    "\n",
    "# 상태 정의\n",
    "class AgentState(TypedDict):\n",
    "    messages: Annotated[Sequence[BaseMessage], operator.add]  # 메시지\n",
    "    next: str  # 다음으로 라우팅할 에이전트"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "561f72a1",
   "metadata": {},
   "source": [
    "## 에이전트 생성\n",
    "\n",
    "### 도구(tool) 생성\n",
    "\n",
    "이 예제에서는 검색 엔진을 사용하여 웹 조사를 수행하는 에이전트와 플롯을 생성하는 에이전트를 만듭니다. \n",
    "\n",
    "아래에 사용할 도구를 정의합니다.\n",
    "\n",
    "- **Research**: `TavilySearch` 도구를 사용하여 웹 조사를 수행합니다.\n",
    "- **Coder**: `PythonREPLTool` 도구를 사용하여 코드를 실행합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "2c8fd5ed",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_teddynote.tools.tavily import TavilySearch\n",
    "from langchain_experimental.tools import PythonREPLTool\n",
    "\n",
    "# 최대 5개의 검색 결과를 반환하는 Tavily 검색 도구 초기화\n",
    "tavily_tool = TavilySearch(max_results=5)\n",
    "\n",
    "# 로컬에서 코드를 실행하는 Python REPL 도구 초기화 (안전하지 않을 수 있음)\n",
    "python_repl_tool = PythonREPLTool()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "15513689",
   "metadata": {},
   "source": [
    "## Agent 생성하는 Utility 구현\n",
    "\n",
    "LangGraph를 사용하여 다중 에이전트 시스템을 구축할 때, **도우미 함수**는 에이전트 노드를 생성하고 관리하는 데 중요한 역할을 합니다. 이러한 함수는 코드의 재사용성을 높이고, 에이전트 간의 상호작용을 간소화합니다.\n",
    "\n",
    "- **에이전트 노드 생성**: 각 에이전트의 역할에 맞는 노드를 생성하기 위한 함수 정의\n",
    "- **작업 흐름 관리**: 에이전트 간의 작업 흐름을 조정하고 최적화하는 유틸리티 제공\n",
    "- **에러 처리**: 에이전트 실행 중 발생할 수 있는 오류를 효율적으로 처리하는 메커니즘 포함"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d98b6064",
   "metadata": {},
   "source": [
    "다음은 `agent_node`라는 함수를 정의하는 예시입니다. \n",
    "\n",
    "이 함수는 주어진 상태와 에이전트를 사용하여 에이전트 노드를 생성합니다. 이 함수를 나중에 `functools.partial`을 사용하여 호출할 것입니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "86501e3b",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.messages import HumanMessage\n",
    "\n",
    "\n",
    "# 지정한 agent와 name을 사용하여 agent 노드를 생성\n",
    "def agent_node(state, agent, name):\n",
    "    # agent 호출\n",
    "    agent_response = agent.invoke(state)\n",
    "    # agent의 마지막 메시지를 HumanMessage로 변환하여 반환\n",
    "    return {\n",
    "        \"messages\": [\n",
    "            HumanMessage(content=agent_response[\"messages\"][-1].content, name=name)\n",
    "        ]\n",
    "    }"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "97b7130b",
   "metadata": {},
   "source": [
    "**참고**\n",
    "\n",
    "`functools.partial`의 역할\n",
    "\n",
    "`functools.partial`은 기존 함수의 일부 인자 또는 키워드 인자를 미리 고정하여 새 함수를 생성하는 데 사용됩니다. 즉, 자주 사용하는 함수 호출 패턴을 간소화할 수 있도록 도와줍니다.\n",
    "\n",
    "**역할**\n",
    "\n",
    "1. **미리 정의된 값으로 새 함수 생성**: 기존 함수의 일부 인자를 미리 지정해서 새 함수를 반환합니다.\n",
    "2. **코드 간결화**: 자주 사용하는 함수 호출 패턴을 단순화하여 코드 중복을 줄입니다.\n",
    "3. **가독성 향상**: 특정 작업에 맞춰 함수의 동작을 맞춤화해 더 직관적으로 사용 가능하게 만듭니다.\n",
    "\n",
    ">예시코드\n",
    "```python\n",
    "research_node = functools.partial(agent_node, agent=research_agent, names=\"Researcher\")\n",
    "```\n",
    "\n",
    "1. `agent_node`라는 기존 함수가 있다고 가정합니다.\n",
    "   - 이 함수는 여러 개의 인자와 키워드 인자를 받을 수 있습니다.\n",
    "\n",
    "2. `functools.partial`은 이 함수에 `agent=research_agent`와 `names=\"Researcher\"`라는 값을 고정합니다.\n",
    "   - 즉, 이제 `research_node`는 `agent_node`를 호출할 때 `agent`와 `names` 값을 따로 지정하지 않아도 됩니다.\n",
    "   - 예를 들어:\n",
    "     ```python\n",
    "     agent_node(state, agent=research_agent, names=\"Researcher\")\n",
    "     ```\n",
    "     대신,\n",
    "     ```python\n",
    "     research_node(state)\n",
    "     ```\n",
    "     처럼 사용할 수 있습니다."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "37c1a0ab",
   "metadata": {},
   "source": [
    "아래는 `functools.partial`을 사용하여 `research_node`를 생성하는 예시입니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "abe06a6b",
   "metadata": {},
   "outputs": [],
   "source": [
    "import functools\n",
    "from langgraph.prebuilt import create_react_agent\n",
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "\n",
    "# Research Agent 생성\n",
    "research_agent = create_react_agent(ChatOpenAI(model=\"gpt-4o\"), tools=[tavily_tool])\n",
    "\n",
    "# research node 생성\n",
    "research_node = functools.partial(agent_node, agent=research_agent, name=\"Researcher\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "38730567",
   "metadata": {},
   "source": [
    "코드를 실행하여 결과를 확인합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "021b29ef",
   "metadata": {},
   "outputs": [],
   "source": [
    "research_node(\n",
    "    {\n",
    "        \"messages\": [\n",
    "            HumanMessage(content=\"Code hello world and print it to the terminal\")\n",
    "        ]\n",
    "    }\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b5676a48",
   "metadata": {},
   "source": [
    "### Agent Supervisor 생성\n",
    "\n",
    "에이전트를 관리 감독하는 감독자 에이전트를 생성합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "3dcae41e",
   "metadata": {},
   "outputs": [],
   "source": [
    "from pydantic import BaseModel\n",
    "from typing import Literal\n",
    "\n",
    "# 멤버 Agent 목록 정의\n",
    "members = [\"Researcher\", \"Coder\"]\n",
    "\n",
    "# 다음 작업자 선택 옵션 목록 정의\n",
    "options_for_next = [\"FINISH\"] + members\n",
    "\n",
    "\n",
    "# 작업자 선택 응답 모델 정의: 다음 작업자를 선택하거나 작업 완료를 나타냄\n",
    "class RouteResponse(BaseModel):\n",
    "    next: Literal[*options_for_next]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "94186ce4",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "\n",
    "# 시스템 프롬프트 정의: 작업자 간의 대화를 관리하는 감독자 역할\n",
    "system_prompt = (\n",
    "    \"You are a supervisor tasked with managing a conversation between the\"\n",
    "    \" following workers:  {members}. Given the following user request,\"\n",
    "    \" respond with the worker to act next. Each worker will perform a\"\n",
    "    \" task and respond with their results and status. When finished,\"\n",
    "    \" respond with FINISH.\"\n",
    ")\n",
    "\n",
    "# ChatPromptTemplate 생성\n",
    "prompt = ChatPromptTemplate.from_messages(\n",
    "    [\n",
    "        (\"system\", system_prompt),\n",
    "        MessagesPlaceholder(variable_name=\"messages\"),\n",
    "        (\n",
    "            \"system\",\n",
    "            \"Given the conversation above, who should act next? \"\n",
    "            \"Or should we FINISH? Select one of: {options}\",\n",
    "        ),\n",
    "    ]\n",
    ").partial(options=str(options_for_next), members=\", \".join(members))\n",
    "\n",
    "\n",
    "# LLM 초기화\n",
    "llm = ChatOpenAI(model=MODEL_NAME, temperature=0)\n",
    "\n",
    "\n",
    "# Supervisor Agent 생성\n",
    "def supervisor_agent(state):\n",
    "    # 프롬프트와 LLM을 결합하여 체인 구성\n",
    "    supervisor_chain = prompt | llm.with_structured_output(RouteResponse)\n",
    "    # Agent 호출\n",
    "    return supervisor_chain.invoke(state)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "82f1e2d0",
   "metadata": {},
   "source": [
    "## 그래프 구성\n",
    "\n",
    "이제 그래프를 구축할 준비가 되었습니다. 아래에서는 방금 정의한 함수를 사용하여 `state`와 `worker` 노드를 정의합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "fd5e5a07",
   "metadata": {},
   "outputs": [],
   "source": [
    "import functools\n",
    "from langgraph.prebuilt import create_react_agent\n",
    "\n",
    "\n",
    "# Research Agent 생성\n",
    "research_agent = create_react_agent(llm, tools=[tavily_tool])\n",
    "research_node = functools.partial(agent_node, agent=research_agent, name=\"Researcher\")\n",
    "\n",
    "code_system_prompt = \"\"\"\n",
    "Be sure to use the following font in your code for visualization.\n",
    "\n",
    "##### 폰트 설정 #####\n",
    "import platform\n",
    "\n",
    "# OS 판단\n",
    "current_os = platform.system()\n",
    "\n",
    "if current_os == \"Windows\":\n",
    "    # Windows 환경 폰트 설정\n",
    "    font_path = \"C:/Windows/Fonts/malgun.ttf\"  # 맑은 고딕 폰트 경로\n",
    "    fontprop = fm.FontProperties(fname=font_path, size=12)\n",
    "    plt.rc(\"font\", family=fontprop.get_name())\n",
    "elif current_os == \"Darwin\":  # macOS\n",
    "    # Mac 환경 폰트 설정\n",
    "    plt.rcParams[\"font.family\"] = \"AppleGothic\"\n",
    "else:  # Linux 등 기타 OS\n",
    "    # 기본 한글 폰트 설정 시도\n",
    "    try:\n",
    "        plt.rcParams[\"font.family\"] = \"NanumGothic\"\n",
    "    except:\n",
    "        print(\"한글 폰트를 찾을 수 없습니다. 시스템 기본 폰트를 사용합니다.\")\n",
    "\n",
    "##### 마이너스 폰트 깨짐 방지 #####\n",
    "plt.rcParams[\"axes.unicode_minus\"] = False  # 마이너스 폰트 깨짐 방지\n",
    "\"\"\"\n",
    "\n",
    "\n",
    "# Coder Agent 생성\n",
    "coder_agent = create_react_agent(\n",
    "    llm,\n",
    "    tools=[python_repl_tool],\n",
    "    state_modifier=code_system_prompt,\n",
    ")\n",
    "coder_node = functools.partial(agent_node, agent=coder_agent, name=\"Coder\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "5029ab17",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langgraph.graph import END, StateGraph, START\n",
    "from langgraph.checkpoint.memory import MemorySaver\n",
    "\n",
    "# 그래프 생성\n",
    "workflow = StateGraph(AgentState)\n",
    "\n",
    "# 그래프에 노드 추가\n",
    "workflow.add_node(\"Researcher\", research_node)\n",
    "workflow.add_node(\"Coder\", coder_node)\n",
    "workflow.add_node(\"Supervisor\", supervisor_agent)\n",
    "\n",
    "\n",
    "# 멤버 노드 > Supervisor 노드로 엣지 추가\n",
    "for member in members:\n",
    "    workflow.add_edge(member, \"Supervisor\")\n",
    "\n",
    "# 조건부 엣지 추가 (\n",
    "conditional_map = {k: k for k in members}\n",
    "conditional_map[\"FINISH\"] = END\n",
    "\n",
    "\n",
    "def get_next(state):\n",
    "    return state[\"next\"]\n",
    "\n",
    "\n",
    "# Supervisor 노드에서 조건부 엣지 추가\n",
    "workflow.add_conditional_edges(\"Supervisor\", get_next, conditional_map)\n",
    "\n",
    "# 시작점\n",
    "workflow.add_edge(START, \"Supervisor\")\n",
    "\n",
    "# 그래프 컴파일\n",
    "graph = workflow.compile(checkpointer=MemorySaver())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c597bbe1",
   "metadata": {},
   "source": [
    "그래프를 시각화합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2d0a8e3c",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_teddynote.graphs import visualize_graph\n",
    "\n",
    "visualize_graph(graph)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0c2bf2b1",
   "metadata": {},
   "source": [
    "## 팀 호출\n",
    "\n",
    "생성된 그래프를 통해 이제 성능을 확인할 수 있습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "215d94b4",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.runnables import RunnableConfig\n",
    "from langchain_teddynote.messages import random_uuid, invoke_graph\n",
    "\n",
    "# config 설정(재귀 최대 횟수, thread_id)\n",
    "config = RunnableConfig(recursion_limit=10, configurable={\"thread_id\": random_uuid()})\n",
    "\n",
    "# 질문 입력\n",
    "inputs = {\n",
    "    \"messages\": [\n",
    "        HumanMessage(\n",
    "            content=\"2010년 ~ 2024년까지의 대한민국의 1인당 GDP 추이를 그래프로 시각화 해주세요.\"\n",
    "        )\n",
    "    ],\n",
    "}\n",
    "\n",
    "# 그래프 실행\n",
    "invoke_graph(graph, inputs, config)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "langchain-kr-lwwSZlnu-py3.11",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
